<?php 

namespace pmvc\mvc;

use pmvc\util\StringUtil;
use pmvc\log\Log;
use pmvc\ApplicationContext;
use Exception;

class Dispatcher {

	const ONE_MEGABYTE				= 1048576;
	const REDIRECT_VIEWNAME_PREFIX	= "redirect:";
	const ERROR_VIEWNAME_PREFIX		= "errorController:";

	private $applicationContext	= NULL;
	private $url				= NULL;
	private $contextUrl			= NULL;
    private $requestedUrl		= FALSE;
    private $outputStream		= NULL;

    /**
     * Creates the Dispatcher.
     * @param ApplicationContext $applicationContext
     * @param string $url the URL
     */
    public function __construct(ApplicationContext $applicationContext, $url, $outputBufferSize=self::ONE_MEGABYTE) {
    	$this->applicationContext = $applicationContext;
		$this->initialize($url);
		$this->outputStream = fopen(
			"php://temp/maxmemory:".$outputBufferSize, "r+");
    }

    /**
     * Returns the output stream.
     */
    public function getOutputStream() {
    	return $this->outputStream;
    }

    /**
     * Clears the output stream.
     */
    public function clearOutputStream() {
    	ftruncate($this->outputStream, 0);
    }

    /**
     * Initializes the Dispatcher for the given url.
     * @param string $url
     */
    public function initialize($url) {
    	
    	// parse url
        $this->url = (strpos($url, '?')===FALSE)
            ? $url 
            : substr($url, 0, strpos($url, '?'));

        // parse context url
        $parts = explode("/", getenv("SCRIPT_NAME"));
        $this->contextUrl = (count($parts)>0)
        	? str_replace("/".$parts[count($parts)-1], "", getenv("SCRIPT_NAME"))
        	: "";
       	Log::trace("context url is {0}", $this->contextUrl);

       	// parse requested url
       	if ($this->contextUrl!="") {
	    	$pos = strpos($this->url, $this->contextUrl);
	    	$this->requestedUrl = ($pos!==FALSE && $pos==0) 
	    		? substr($this->url, strlen($this->contextUrl)) : $this->url;
       	} else {
       		$this->requestedUrl = $this->url;
       	}
	    Log::trace("requestedUrl parsed as {0}", $this->requestedUrl);
    }

    /**
     * Returns the URL that was requested.
     * @return string the URL
     */
    public function getRequestedUrl() {
    	return $this->requestedUrl;
    }

    /**
     * Returns the context URL
     * @return string the context URL
     */
    public function getContextUrl() {
    	return $this->contextUrl;
    }

    /**
     * Returns the context URL
     * @return string the context URL
     */
    public function getBaseUrl() {
    	$url = "";

    	// get protocol
    	$http = array_key_exists("HTTPS", $_SERVER)
    		? "https" : "http";
  
    	// get host name if we can
    	if (array_key_exists("HTTP_HOST", $_SERVER)) {
    		$url .= $http."://".$_SERVER["HTTP_HOST"];
    	} else if (array_key_exists("SERVER_NAME", $_SERVER)) {
    		$url .= $http."://".$_SERVER["SERVER_NAME"];
    	} else if (array_key_exists("SERVER_ADDR", $_SERVER)) {
    		$url .= $http."://".$_SERVER["SERVER_ADDR"];
    	}
   
    	// a special port?
    	if (strlen($url)>0
    		&& array_key_exists("SERVER_PORT", $_SERVER)
    		&& $_SERVER["SERVER_PORT"]!=80
    		&& $_SERVER["SERVER_PORT"]!=443) {
    		$url .= ":".$_SERVER["SERVER_PORT"];
    	}

    	// add the context and return
    	return $url.$this->contextUrl;
    }

    /**
     * Returns the URL that was requested.
     * @return string the URL
     */
    public function getApplicationContext() {
    	return $this->applicationContext;
    }

    /**
     * Dispatches the request.
     */
    public function dispatch() {
    	Log::trace("Dispatching request");
    	$dispatchStartTime = microtime(TRUE);
   
    	// start session
    	session_start();

    	// create a model
    	$model = new Model();

    	// run request wrappers
    	if (!$this->dispatch_RequestWrapper($model, true)) {
	    	Log::trace("Dispatcher took {0} milliseconds", (microtime(TRUE)-$dispatchStartTime)*1000);
			return;
    	}

    	// run controller
    	$viewName = $this->dispatch_Controller($model);
    	
    	// run view handler
    	$wasRedirected = false;
    	if (pmvc_hasVal($viewName)) {
    		
    		// check for redirect
    		if (StringUtil::startsWith($viewName, self::REDIRECT_VIEWNAME_PREFIX)) {
    			$toUrl = substr($viewName, strlen(self::REDIRECT_VIEWNAME_PREFIX));
    			$toUrl = ModelUtil::stringReplace($toUrl, $model);
    			if (!StringUtil::startsWith($toUrl, "http", TRUE)
    				&& !StringUtil::startsWith($toUrl, "/")) {
    				$toUrl = $this->getBaseUrl()."/".$toUrl;
    			}
    			Log::debug("Redirecting to {0}", $toUrl);
    			$wasRedirected = true;
    			header('Location:' . $toUrl);
    			
    		} else {
    			$this->dispatch_viewHandler($model, $viewName);
    		}

    	// no view
    	} else {
			Log::debug(
		        "No view name returned from controller, assuming controller handled it");
    	}
   
    	// run request wrappers
    	$this->dispatch_RequestWrapper($model, false);

    	// print the output buffer
    	if (!$wasRedirected) {
	    	rewind($this->outputStream);
	    	$stat = fstat($this->outputStream);
	    	header("Content-Length: ".$stat['size']);
	    	fpassthru($this->outputStream);
	    	fclose($this->outputStream);
    	}

    	// log elapsed time
    	Log::trace("Dispatcher took {0} milliseconds", (microtime(TRUE)-$dispatchStartTime)*1000);
    }

    /**
     * Dispatches RequestWrappers.
     * @param bool $beforeRequest
     */
    private function dispatch_RequestWrapper(Model $model, $beforeRequest) {
		Log::trace("Running RequestWrappers");
    	
        // get request mapper handler mapping
    	$wrapperMappings = $this->applicationContext->getObjectsOfType("pmvc\mvc\RequestWrapperMapping");
    	foreach ($wrapperMappings as $wrapperMapping) {
    		$requestWrappers = $wrapperMapping->getRequestWrappers($this->requestedUrl);
    		if (pmvc_hasval($requestWrappers) && count($requestWrappers)>0) {
	    		foreach ($requestWrappers as $wrapper) {
    				Log::trace("RequestWrapper: {0}", get_class($wrapper));
					if ($beforeRequest) {
						if ($wrapper->beforeRequest($model, $this)===FALSE) {
							Log::debug(
						        "Request wrapper canceled further execution: {0}", get_class($wrapper));
							return FALSE;
						}
					} else {
						$wrapper->afterRequest($this);
					}
	    		}
    		}
    	}
    	return TRUE;
    }

    /**
     * Dispatches {@link Controller}s.
     */
    private function dispatch_Controller(Model $model) {
    	
        // get controller mappings
    	$controllerMappings = $this->applicationContext->getObjectsOfType("pmvc\mvc\ControllerMapping");
    	// get controller for requested URL
    	$controller = $this->getControllerForUrl($controllerMappings, $this->requestedUrl);

    	if (!isset($controller)) {
    		// get a 404 controller
    		$controller = $this->getControllerForUrl($controllerMappings, self::ERROR_VIEWNAME_PREFIX.'404');
    		if (!isset($controller)) {
    			$controller = new \pmvc\mvc\controller\FourOhFourController();
    		}
    	}
    	Log::trace("Controller: {0}", get_class($controller));
    	
    	try {
	    	return $controller->handleRequest($model, $this);
	    	
    	} catch(Exception $e) {
    		// get a 500 controller
    		$controller = $this->getControllerForUrl($controllerMappings, self::ERROR_VIEWNAME_PREFIX.'500');
    		if (isset($controller)) {
    			$model['exception'] = $e;
    		} else {
    			$controller = new \pmvc\mvc\controller\ExceptionController($e);
    		}
    		Log::error("Exception during dispatch, deferring to controller: {0}", get_class($controller));
    		return $controller->handleRequest($model, $this);
    	}
    }
    
    /**
     * Gets a controller for a URL.
     * @param Array $controllerMappings
     * @param string $url
     */
    private function getControllerForUrl(Array $controllerMappings, $url) {
        foreach ($controllerMappings as $controllerMapping) {
    		$controller = $controllerMapping->getController($url);
    		if (isset($controller)) {
    			return $controller;
    			break;
    		}
    	}
    	return NULL;
    }

    /**
     * Dispatches {@link ViewHandler}s.
     */
    private function dispatch_viewHandler(Model $model, $viewName) {
    	
        // get view handler mappings
    	$viewHandlerMappings = $this->applicationContext->getObjectsOfType("pmvc\mvc\ViewHandlerMapping");
    	$viewHandler = NULL;
    	foreach ($viewHandlerMappings as $viewHandlerMapping) {
    		$viewHandler = $viewHandlerMapping->getViewHandler($viewName);
    		if (pmvc_hasval($viewHandler)) {
    			break;
    		}
    	}

    	// invoke the view handler
    	if (pmvc_hasVal($viewHandler)) {
    		Log::trace("ViewHandler: {0}", get_class($viewHandler));
    		return $viewHandler->renderView($model, $viewName, $this);
    		
    	} else {
        	Log::warn(
        		"No ViewHandler configured for view {0}", 
        		$viewName);
    	}
    }
}

?>